#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the version 3 of the GNU Lesser General Public License
#   as published by the Free Software Foundation.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Copyright (c) NEC Deutschland GmbH, NEC HPC Europe
#
# $Id$
import logging
import re
from copy import deepcopy
from inspect import isfunction
from threading import Thread


__all__ = ["Component", "Components"]


class Component():
    """
    Components are objects that live within an aggmon installation.
    Currently there are Importers, Aggregators, Databases, Hierarchy.

    Arguments, etc...:

    cmd : a string that is eval/exec-able and creates the component instance.
          This is the output of the configuration information.

    name : used to identify this component. Should be unique, used as dict key for the alive info.

    requires_re : array of regexps matching component names which need to be started before this
                  component is brought up.

    Each component that uses this infrastructure (Importer, Database, etc.) should have a corresponding
    class derived from this one which implements the missing methods (start, stop, etc.).

    Target is to componentize following entities in aggmon: hierarchy, database, directrpc, importers,
    aggregators, channels.
    """

    # component status
    STOPPED = 0
    RUNNING = 1

    def __init__( self, cmd=None, args=None, kwds=None, name=None, requires_re=None ):
        self.cmd = {}
        self.cmd["class"] = cmd
        if args is not None:
            self.cmd["args"] = args
        else:
            self.cmd["args"] = []
        if kwds is not None:
            self.cmd["kwds"] = kwds
        else:
            self.cmd["kwds"] = {}
        self.instance = None
        self.name = name
        assert requires_re is None or isinstance( requires_re, list ), "requires_re for '%s' must be an array!" % name
        self.requires_re = requires_re
        self.state = Component.STOPPED

        # The 'depends' are components that depend on this one to be started.
        # With them and the 'requires' we can follow both directions of the dependency tree.
        # 'depends'/'requires' are computed/updated from outside.
        self.depends = []
        self.requires = []

    def eq( self, component ):
        """
        Compare this component with another one: are they equal?
        By default just compare the cmd strings. More involved
        checks could contain checksumming of configuration files.
        """
        return self.cmd == component.cmd

    def get_instance( self ):
        return self.instance

    # is this needed?
    def is_runnable( self ):
        return isinstance( self.instance, Thread )

    def instantiate( self, *args, **kwds ):
        raise NotImplementedError

    def start( self, **kwds ):
        if self.status() == Component.RUNNING:
            logging.error( "Tried to start RUNNING component: %s" % self.cmd )
            return
        k = {}
        for key, val in self.cmd["kwds"].items():
            if isfunction( val ):
                k[key] = val()
            else:
                k[key] = val
        for key, val in kwds.items():
            if key in self.allowed_kwds:
                if isfunction( val ):
                    k[key] = val()
                else:
                    k[key] = val

        try:
            self.instantiate( *self.cmd["args"], **k )
            if self.is_runnable():
                self.instance.start()
        except Exception, e:
            self.state = Component.STOPPED
            logging.error( "Starting component %s failed. Exception: %s" % (self.__class__.__name__, str(e)) )
            raise e
        else:
            self.state = Component.RUNNING

    def status( self ):
        return self.state

    def stop( self ):
        raise NotImplementedError


class Components(dict):
    # class variable that holds a dict of alive components
    alive = {}

    def __init__( self ):
        self = {}
        #parse configs here?

    def add( self, component, update_reqs=False, update_deps=False ):
        self[component.name] = component
        if update_reqs:
            self.update_reqs( component )
        if update_deps:
            for comp in self.values():
                self.update_deps( comp )

    def compare_alive( self ):
        "Find out whether this components object and the 'alive' one are the same."
        pass

    def depends( self, component_name ):
        """
        Array of arrays of components depending of a given component.
        These are the components that require (directly or indirectly) that the given
        component (passed in as argument) is started.
        Walk through it forwards when stopping.
        """
        deps = []
        # if empty string: return components that depend on nothing
        if len( component_name ) == 0:
            for cname, v in self.items():
                if len( v.depends ) == 0:
                    deps.append( cname )
            return deps
        # and now the normal case
        if not component_name in self.keys():
            return None
        cnames = [component_name]
        # avoid circular dependencies
        maxiter = 15
        while maxiter > 0:
            maxiter -= 1
            new_deps = {}
            for cname in cnames:
                if not cname in self.keys():
                    logging.error( "Component name '%s' not found among components!?" % cname )
                    break
                comp = self[cname]
                if len( comp.depends ) > 0:
                    for d in comp.depends:
                        new_deps[d] = 1
            cnames = new_deps.keys()
            if len( cnames ) == 0:
                break
            deps.append( deepcopy( cnames ) )
        deps.reverse()
        return deps

    def make_alive( self ):
        """
        For components that didn't change: replace the instance from the alive thing into this one.
        Added components with running dependencies: start them.
        Changed components: stop them (after having stopped their dependencies in the alive components)
        then start them in this components instance (instantiate), start all those who depend on it.
        """
        # the simple version first
        Components.alive = self

    def requires( self, component_name ):
        """
        Array of arrays of requirements for a given component.
        These are lists of components that need to be started before the given component is started.
        Work through it forwards when starting.
        """
        reqs = []
        # if empty string: return components that require nothing
        if len( component_name ) == 0:
            for cname, v in self.items():
                if len( v.requires ) == 0:
                    reqs.append( cname )
            return reqs
        # and now the normal case
        if not component_name in self.map:
            return None
        cnames = [component_name]
        # avoid circular dependencies
        maxiter = 15
        while maxiter > 0:
            maxiter -= 1
            new_reqs = {}
            for cname in cnames:
                if not cname in self.keys():
                    logging.error( "Component name '%s' not found among components!?" % cname )
                    break
                comp = self[cname]
                if len( comp.requires ) > 0:
                    for d in comp.requires:
                        new_reqs[d] = 1
            cnames = new_reqs.keys()
            if len( cnames ) == 0:
                break
            reqs.append( deepcopy( cnames ) )
        return reqs

    def start_all( self, **kwds ):
        "Start all components in the correct order."
        # first: find components that have no 'requires'
        started = []
        undone = self.requires( "" )
        while len( undone ) > 0:
            for cname in undone:
                comp = self[cname]
                reqs = comp.requires
                non_started_reqs = [ req for req in reqs if not req in started ]
                if len( non_started_reqs ) > 0:
                    continue
                # start this component
                try:
                    comp.start( **kwds )
                except Exception, e:
                    logging.error( "Failed to start component '%s', exception: %s" % (cname, str(e)) )
                    raise e
                else:
                    started.append( cname )
                    undone.remove( cname )
                    # add components that depend on the started one
                    for dep in comp.depends:
                        # does 'dep' have unstarted requires?
                        unstarted_reqs = [ req for req in self[dep].requires if req not in started ]
                        if len( unstarted_reqs ) == 0 and not dep in undone:
                            undone.append( dep )

    def start_component( self, component_name, start_deps=False, **kwds ):
        """
        Start a component after having made sure that its dependencies are also started.
        After starting the component optionally check all depends and start them, if their
        requires are all fulfilled.
        """
        pass

    def stop_all( self ):
        "Stop all components in the correct order."
        pass

    def stop_component( self, component_name ):
        """
        Stop one component, but after having stopped all other components that depend on it.
        """
        pass

    def update_deps( self, component ):
        "Update the depends of a component, done some time at the end, when all _requires_ are known."
        depends = []
        for cname, comp in self.items():
            if len( comp.requires ) > 0:
                for req in comp.requires:
                    if re.search( component.name, req ):
                        depends.append( cname )
        if len( depends ) > 1:
            depends.sort()
        component.depends = depends

    def update_all_deps_reqs( self ):
        for comp in self.values():
            self.update_reqs( comp )
        # do this _after_ all reqs are known!
        for comp in self.values():
            self.update_deps( comp )

    def update_reqs( self, component ):
        "Update the requires of a component, usually done when added."
        requires = []
        if component.requires_re is not None:
            match_string = "|".join( component.requires_re )
            for component_name in self.keys():
                if re.search( match_string, component_name ):
                    requires.append( component_name )
        component.requires = requires
